Skip to content

fix(eks): set resource requests/limits for Calico/Tigera components - #343

Merged
amdove merged 8 commits into
mainfrom
helm-resource-requests-limits
Aug 7, 2026
Merged

fix(eks): set resource requests/limits for Calico/Tigera components#343
amdove merged 8 commits into
mainfrom
helm-resource-requests-limits

Conversation

@amdove

@amdove amdove commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Description

Sets resource requests and limits on the Calico/Tigera components installed by the tigera-operator Helm chart on EKS. Previously the chart shipped resources: {} and the operator has no defaults of its own, so these pods had no reservations and no ceilings and competed unbounded with workloads on busy nodes.

Policy:

  • Memory: request == limit. Non-compressible, so equal request/limit gives a guaranteed floor and a ceiling that protects the node from a runaway component.
  • CPU: request only, no limit. Avoids CFS-throttling the dataplane (a throttled calico-node degrades networking cluster-wide). Wikimedia reached the same conclusion in production: T277877.
Component CPU request Memory req = limit
tigera-operator 250m 384Mi
calico-node 250m 512Mi
calico-typha 100m 256Mi
calico-kube-controllers 50m 192Mi
calico-apiserver 100m 256Mi
csi-node-driver (calico-csi, csi-node-driver-registrar) 10m each 64Mi each

Nothing was reserved before this, so these are new reservations rather than adjustments. Reserved memory added per node: 512Micalico-node is the only per-node DaemonSet running, and everything else is a single-replica Deployment that lands on one node.

Sizing

Memory bounds are round values above the highest working set observed across the fleet over 7 days (control-room Mimir, max_over_time(container_memory_working_set_bytes[7d]) across the Calico namespaces):

Component Fleet peak Bound
calico-node 374.8 MiB 512Mi
tigera-operator 245.5 MiB 384Mi
calico-typha 131.6 MiB 256Mi
calico-kube-controllers 123.5 MiB 192Mi
calico-apiserver 76.7 MiB 256Mi

tigera-operator and calico-kube-controllers were originally 256Mi and 128Mi, within 4% of their observed peaks, and were raised once measured. The others already had room and are unchanged.

Headroom is deliberately modest rather than a round 2×, because request == limit makes it reserved capacity, not just a kill threshold. calico-node has the least (~1.4×) and is the one to watch, since it's the only per-node DaemonSet and any increase lands on every node.

CPU requests are floors, not caps, since nothing sets a CPU limit. calico-node peaks near 750m against its 250m request and stays there — raising it would reserve capacity on every node without preventing anything. tigera-operator peaks near 350m and goes to 250m.

The 250m on calico-node matches upstream: across the self-managed install manifests at v3.31.4 it's the only resource value set on any component, with no memory value anywhere. projectcalico#5418, asking for recommendations, was closed as not planned.

Code Flow

All in deployTigeraOperator (lib/steps/eks_helpers.go). calicoResources builds the {requests: {cpu, memory}, limits: {memory}} block; calicoContainer pairs a container name with it; calicoComponentOverride wraps those in the operator's strategic-merge shape (variadic, since csi-node-driver has two containers).

Values map to installation.calicoNodeDaemonSet / typhaDeployment / calicoKubeControllersDeployment / csiNodeDriverDaemonSet, plus top-level resources for the operator pod and apiServer.apiServerDeployment for calico-apiserver.

csi-node-driver is bounded but currently inert. The operator enables Calico CSI whenever installation.kubeletVolumePluginPath is unset, which is what PTD leaves it as, so a cluster built today runs this DaemonSet on every node. Existing clusters carry an out-of-band "None" that disables it, and no calico-csi container appears in fleet metrics. Ownership of that field is tracked separately.

Scope

EKS: fixed. PTD installs the chart here, so the reservations attach directly to the release.

AKS: not fixed. AKS Calico comes from the Azure-managed network-policy add-on. Anything in kube-system is Microsoft-managed and customers can't alter it, so the Installation CR is Azure's to reconcile. Fixing it there means migrating to self-managed Calico or moving the dataplane to Cilium, which Microsoft recommends — otherwise node right-sizing is the only lever.

Watch after applying

  • OOMKills — memory req == limit means overshoot is an OOMKill, not a burst. calico-node has the thinnest margin and its footprint grows with endpoints/policies.
  • Rollout churn — applying resources restarts the calico-node DaemonSet and the operator-managed deployments; expect a brief per-node CNI reconcile.

Category of change

  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I have reviewed my own diff and added inline comments on lines I want reviewers to focus on or that I am uncertain about

Testing

just format, go vet ./..., and just test-lib pass on the branch merged with main. TestAWSEKSDeployTigeraResourceRequests asserts each component's values and the no-CPU-limit invariant.

The tigera-operator chart ships resources: {} and the operator's
built-in component defaults set no memory bounds, so the Calico pods
had no guaranteed reservations and no ceilings — a source of resource
contention on busy nodes.

Pin predictable reservations on the operator pod, calico-node,
calico-typha, calico-kube-controllers, and calico-apiserver, following
a memory-bounded / CPU-unbounded policy: memory request == limit
(non-compressible; guaranteed floor + bounded ceiling), CPU request
only with no CPU limit (avoids CFS-throttling the dataplane).

EKS only; AKS uses the managed Calico add-on.
@amdove
amdove marked this pull request as ready for review July 10, 2026 21:25
@amdove
amdove requested a review from a team as a code owner July 10, 2026 21:25
stevenolen
stevenolen previously approved these changes Jul 13, 2026
@amdove

amdove commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Jul 17, 2026

Copy link
Copy Markdown

Claude finished @amdove's task in 1m 0s —— View job


Code Review

  • Read changed files
  • Review implementation
  • Review tests

This is a clean, well-scoped change. The implementation is correct and the rationale is sound. A few observations:

lib/steps/eks_helpers.go

The two helpers are a nice abstraction. calicoResources correctly encodes the memory-bounded / CPU-unbounded policy — memory appears in both requests and limits, CPU appears only in requests. calicoComponentOverride mirrors the operator's strategic-merge shape accurately.

One minor note: calicoComponentOverride only patches containers, not initContainers. For calico-node in particular, the operator injects an install-cni init container. Init containers are ephemeral (they exit after CNI installation and don't run during normal operation), so omitting them here is fine — they don't contribute to steady-state resource pressure. Worth being aware of if OOM pressure appears during node bootstrap.

The calico-apiserver override is placed under apiServer.apiServerDeployment rather than under installation, which matches the chart's schema for that component. That's the right call — a comment noting why it lives outside installation would help future readers who might wonder why it's inconsistent, but it's not blocking.

Resource values

The chosen values look reasonable for typical cluster sizes. calico-node at 512Mi is comfortable headroom for Felix on moderately-sized clusters. The note in the PR body about watching OOM/restart counts on the largest clusters is important operational context.

One thing to verify if you haven't already: the tigera-operator chart's resources key at the top level applies to the operator Deployment (not the Installation CR), which is what you want. The chart's values schema does support a top-level resources block for the operator pod, so this should work as written, but it's worth confirming against the chart version in use.

lib/steps/eks_aws_test.go

TestAWSEKSDeployTigeraResourceRequests is thorough and correctly asserts the no-CPU-limit invariant (assert.NotContains(..., "cpu")). The containerResources helper navigates the deep nested path cleanly. The test correctly checks all five components including the operator pod's top-level resources block.

Overall

No issues blocking merge. The change is correct, well-tested, and appropriately scoped to EKS. The AKS limitation is clearly documented. Approved by stevenolen — LGTM.

Address review feedback: note that calicoComponentOverride patches only
the main container (not ephemeral initContainers like install-cni), and
explain why calico-apiserver's resources live under apiServer rather than
installation (they map to the separate APIServer CR).
amdove added 2 commits August 7, 2026 09:31
…nale

The tigera operator ships no resource defaults of its own — an Installation
without the deprecated ComponentResources renders empty resources — so the
previous comment claiming calico-node "keeps the operator's built-in 250m CPU
request floor" was wrong. 250m is the request upstream sets on calico-node in
the self-managed manifests/calico.yaml; under the operator it is a new per-node
reservation. Comment corrected to say so.

Also bounds csi-node-driver, which was still running unbounded on every node:
the operator enables the Calico CSI plugin unless kubeletVolumePluginPath is
"None", and PTD leaves it at the default. Both of its containers (calico-csi
and csi-node-driver-registrar) are small and idle once the driver registers
with kubelet, so they take the smallest bounds of any component here.

calicoComponentOverride now takes a variadic container list (via a new
calicoContainer helper) to express the two-container CSI DaemonSet, and the
test looks containers up by name instead of by position.
@amdove

amdove commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

@claude review again please

@claude

claude Bot commented Aug 7, 2026

Copy link
Copy Markdown

Claude finished @amdove's task in 54s —— View job


Code Review

  • Gather context
  • Read changed files
  • Review implementation
  • Review tests
  • Post findings

The implementation is correct and well-structured. No issues blocking merge. Here are the findings:

lib/steps/eks_helpers.go

The three helpers (calicoResources, calicoContainer, calicoComponentOverride) layer cleanly. The memory-bounded / CPU-unbounded invariant is encoded exactly once in calicoResources, so it can't be violated by a future copy-paste.

calicoComponentOverride takes ...pulumi.Input rather than ...pulumi.Map. That's intentional — calicoContainer returns a pulumi.Map which satisfies pulumi.Input, and pulumi.Array(containers) needs []pulumi.Input. No issue here, just worth knowing that the variadic type is slightly looser than the actual usage.

The apiServer block placement is correct. The tigera-operator chart treats the apiServer.apiServerDeployment path as a top-level values key distinct from installation, and the comment explains why. One thing to verify: apiServer.enabled defaults to true starting in v3.28 of the chart. If the pinned version is older, the apiServer override exists but the apiserver Deployment may not be running. Low risk — the override is harmless if the workload isn't scheduled — but worth a quick check against the version string in eks_helpers.go.

Resource values

The values are defensible. calico-typha at 256Mi is the one to watch: Typha memory scales with the number of nodes (it aggregates Felix syncs), so a large cluster (50+ nodes) can push Typha above this. The PR body flags OOMKills as the signal to watch, which is the right operational posture.

calico-kube-controllers at 128Mi is conservative. The controller manages IPAM and policy GC; on clusters with heavy NetworkPolicy churn it can spike. Worth bumping to 256Mi if you see restarts, but 128Mi is a fine starting point.

lib/steps/eks_aws_test.go

TestAWSEKSDeployTigeraResourceRequests is the right level of coverage. A few specific observations:

  1. assertMemoryBounded applied to operator resources directly (line 425): values["resources"].ObjectValue() returns the flat {requests: {cpu, memory}, limits: {memory}} map, which is exactly what assertMemoryBounded expects. Correct.

  2. assert.NotContains on the limits map (line 408): checks that the "cpu" key is absent from limits. This is the strongest possible assertion for the no-CPU-limit invariant — a future edit that accidentally adds a CPU limit will fail the test. Good.

  3. Container lookup by name, not index (lines 390–398): The containerResources helper iterates by name rather than relying on slice position, so reordering containers in calicoComponentOverride calls won't silently break assertions. Good defensive design.

  4. require.Failf inside a closure (line 397): require.Failf calls t.FailNow(), which is safe in a regular goroutine but can interact oddly with some test runners if the closure were ever called from a goroutine. Here it's called synchronously, so no issue. Just worth noting if this pattern is copied elsewhere.

Overall

No blocking issues. Implementation matches the schema, policy is correctly encoded and tested, and the scoping rationale (EKS fixed, AKS out of scope) is clear.

@amdove
amdove requested a review from stevenolen August 7, 2026 17:20
amdove added 4 commits August 7, 2026 11:37
Measured peak working set across the fleet over 7 days showed two components
sitting at ~1.04x their ceiling, close enough to OOMKill on any growth:

  tigera-operator          245.5 MiB against 256Mi
  calico-kube-controllers  123.5 MiB against 128Mi

Both are raised to ~1.5x observed peak (384Mi and 192Mi). Both are
single-replica Deployments, so neither adds anything to the per-node
reservation, which stays at 512Mi for calico-node.

calico-node keeps 512Mi: at 374.8 MiB observed that is already ~1.37x, and
raising it would reserve another 256Mi on every node. typha (1.94x) and
apiserver (3.34x) already clear the bar and are unchanged.

tigera-operator's CPU request goes 100m -> 250m; it peaks near 350m and is a
single pod, so the reservation is negligible. calico-node stays at 250m
despite peaking near 750m: with no CPU limit that peak isn't throttled, and
raising the request would cost capacity on every node.

~1.5x rather than 2x because request == limit makes headroom reserved
capacity rather than a free kill threshold.

Also corrects the csi-node-driver comment: it does not run on every node
today. The operator enables CSI when kubeletVolumePluginPath is unset (which
PTD leaves it as), but existing clusters carry an out-of-band "None" that
disables it, so the override is inert until that is resolved.
The comment claimed all bounds were ~1.5x observed peak, but only the two
that were raised land there. 1.5x was the threshold for deciding what needed
headroom, not a target every value hits: typha and apiserver were already
above it and left alone, and calico-node sits at ~1.37x deliberately because
it is the only per-node DaemonSet.
The multipliers aren't a design target and the cost of memory doesn't vary by
component — each bound is just a round value above the measured peak. Says
that instead.
@amdove
amdove added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 845574c Aug 7, 2026
5 checks passed
@amdove
amdove deleted the helm-resource-requests-limits branch August 7, 2026 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants